安安~我是ChiYu~
昨天剛把 EF Core 與通知 SDK 留在 Use Case 外圍,今天我就故意把 WorkItemsDbContext 加回 OverdueWorkItemProcessor 的建構式。Release Build 通過,既有的 ProductionBoundaryTests 也全部通過。
Code 已經違反 Dependency Rule,測試卻還是綠的。
測試沒有判斷錯,它根本沒看到這個 Processor。
原本的 Architecture Test 只掃描 src/WorkItems.Api/UseCases,OverdueWorkItemProcessor.cs 卻位於 API 根目錄。保留同一個 DbContext 違規、補完整 Discovery 後,測試立即轉紅;移除依賴、讓 Processor 只認識 Use Case Port 後,才重新得到有意義的綠燈。
這次最後接受 Source/Namespace Test,不是因為它能保證整個架構永遠正確,而是目前高階型別不多,先用明確的所有權、非空掃描與受控違規,就能攔住已知風險。等高階政策持續增加、多人維護或越界反覆發生,再升級成獨立 Assembly。
今天要檢查的因此不是測試名稱寫得多完整,而是它實際保護哪些 Code。Agent 建立 Interface 或 Architecture Test 後,如果受測集合根本漏掉高階政策,最後的全綠摘要仍然沒有驗收價值。
整篇可以先抓住一條「假綠燈 → 真紅燈 → 真綠燈」的路徑:
| 階段 | 同一個 DbContext 違規發生什麼事 |
這一步要證明什麼 |
|---|---|---|
| 假綠燈 | Processor 直接依賴 DbContext,舊測試仍通過 |
問題出在受保護型別的發現範圍,不是違規突然合理 |
| 真紅燈 | 保留相同違規,只補完整 Discovery 與非空檢查 | Architecture Test 確實看得見並拒絕這項依賴 |
| 真綠燈 | 規則不變,Processor 改依賴 Use Case 擁有的 Port | 結構規則與既有 API/Outbox 行為能同時通過 |
這樣設計,是為了避免「修完後測試全綠」變成循環論證。規則必須先對同一項錯誤亮紅燈,才有資格替修正後的綠燈背書。
先用 Work Item API 把四個容易混在一起的概念分開:
| 名詞 | 它回答的問題 | Work Item API 中的例子 |
|---|---|---|
| 執行時控制流 | 系統執行時,呼叫會走到哪裡? | Processor 呼叫 Persistence Port,最後由 EF Core Adapter 存進 SQLite |
| 原始碼依賴 | 編譯某個類別時,它必須認識哪些型別? | Processor 若直接接收 WorkItemsDbContext,就必須認識外層 Persistence 型別 |
| Composition Root | Interface 與具體實作在哪裡完成組裝? | Program.cs 把 Use Case Port 綁定到 EF Core 或 SDK Adapter |
| Dependency Rule | 哪一層可以在原始碼中認識哪一層? | 外層 Adapter 可以認識 Use Case Port;Use Case 不引用 EF Core、MVC 或 SDK 型別 |
呼叫最後會抵達資料庫,不代表 Use Case 必須引用 DbContext。Use Case 定義自己需要的 Port,外層 Adapter 實作,Program.cs 再完成組裝。控制流可以往外,原始碼依賴仍指向高階政策。
Clean Architecture 經常畫成四個同心圓:Entities、Use Cases、Interface Adapters,以及 Frameworks and Drivers。這些圓圈是在區分責任與政策層次,不是要求每個專案固定拆成四個資料夾或四個 Assembly。
| 區域 | 負責什麼 | Work Item API 中的例子 |
|---|---|---|
| Entities | 最穩定、最通用的核心業務規則 | WorkItems.ServiceLevel 內的服務等級政策 |
| Use Cases | 編排某個應用程式情境與所需能力 | OverdueWorkItemProcessor、通知與 Persistence Port |
| Interface Adapters | 翻譯核心與外部技術之間的資料與呼叫 | Controller、EF Core Adapter、通知 SDK Adapter |
| Frameworks and Drivers | Web Framework、資料庫與第三方工具 | ASP.NET Core、EF Core、SQLite、通知 SDK |
別被 WorkItem 的 Entity 名稱誤導。它目前負責 EF Core Persistence Mapping 與 Change Tracking,因此仍是外層資料實作;類別名稱叫 Entity,不會自動讓它成為最內圈的核心規則。
判斷角色要看它承擔什麼責任。更換資料庫或 SDK 時,Use Case 是否也被迫修改,才是今天真正要檢查的問題。
DbContext 越界,還需要能拒絕錯誤依賴的檢查Uncle Bob 在近期訪談裡分享,他會讓 Agent 產生 Architecture Viewer,把模組與依賴方向攤開,再由確定性檢查工具驗證哪些依賴允許存在。Agent 若建立被禁止的依賴,就得反轉依賴、加入合適介面,或重新拆分模組。
這比在 Prompt 裡寫「請遵守 Clean Architecture」多了一道真正的拒絕機制。文字說明高階政策為什麼不能認識 EF Core;Dependency Checker 則確認最後的 Code 有沒有引用 DbContext。
但工具只會檢查已列入規則與 Discovery 範圍的型別。高階政策若被放在測試找不到的位置,錯誤依賴仍可能維持綠燈。
今天讓同一個 DbContext 違規先漏過、再轉紅,就是要確認 Architecture Test 到底看見哪些型別,而不是只看測試名稱聽起來是否完整。
先看執行時方向。HTTP Request 或排程觸發逾期流程後,Use Case 必須存取資料,也必須把通知交給 Provider:
flowchart LR
HTTP["HTTP Controller"] --> InputPort["IOverdueWorkItemProcessor"]
Worker["Background Worker"] --> InputPort
InputPort --> UseCase["OverdueWorkItemProcessor"]
UseCase --> PersistencePort["Persistence Ports"]
UseCase --> NotificationPort["Notification Port"]
PersistencePort --> EFAdapter["EF Core Adapters"]
EFAdapter --> Database["SQLite"]
NotificationPort --> NotificationAdapter["Notification Adapter"]
NotificationAdapter --> SDK["External SDK/Provider"]
這張圖回答執行時誰呼叫誰:Use Case 會透過 Port 走到 EF Core 與通知 SDK。
原始碼依賴問的是另一件事:編譯 Processor 時,它必須認識哪些型別?
flowchart TB
Controller["Controller/Worker"] --> InputPort["Use Case Input Port"]
EFAdapter["EF Core Adapters"] --> PersistencePort["Use Case 擁有的 Persistence Ports"]
NotificationAdapter["Notification Adapter"] --> NotificationPort["Use Case 擁有的 Notification Port"]
EFAdapter --> EFCore["EF Core/SQLite"]
NotificationAdapter --> SDK["External SDK"]
CompositionRoot["Program.cs<br/>Composition Root"] --> InputPort
CompositionRoot --> EFAdapter
CompositionRoot --> NotificationAdapter
外層 Adapter 實作 Use Case 擁有的 Interface,因此原始碼依賴往內指。OverdueWorkItemProcessor 只看得到 IOverdueWorkItemMarker 與 INotificationOutboxDispatcher;實際傳送通知的 IWorkItemNotificationSender 留在 Dispatcher 後方,EF Core 與 SDK 型別都在外圍。
兩邊的綁定集中在 Program.cs:
builder.Services.AddScoped<
INotificationOutboxDispatcher,
EfCoreNotificationOutboxDispatcher>();
builder.Services.AddScoped<
IOverdueWorkItemMarker,
EfCoreOverdueWorkItemMarker>();
builder.Services.AddScoped<
IOverdueWorkItemProcessor,
OverdueWorkItemProcessor>();
執行時,Use Case 會呼叫外層實作;編譯時,外層實作則引用 Use Case 擁有的 Interface。Adapter 可以引用 EF Core 或 SDK,只要引用沒有回流到高階政策。
Interface 本身不會自動反轉依賴。假設 IOverdueWorkItemMarker 定義在 Infrastructure Project,Processor 為了使用它,仍得參考 Infrastructure。Port 必須由需要這項能力的 Use Case 擁有,外層 Adapter 再反過來實作,依賴方向才真的往內。
本文的 Boundary Test 指 ProductionBoundaryTests。它讀取 Production Source 與 Project Reference,檢查架構依賴;它不送 HTTP Request,也不負責驗證 API 回應內容。
實驗從昨天的接受版本開始:
3630a0adcd0de17c70b5740948dbb2b49f26dc47
day-23-clean-boundaries
你可以直接切到相同起點:
git clone https://github.com/eric861129/AI-CleanCode-API-Demo.git
cd AI-CleanCode-API-Demo
git fetch --tags
git switch --detach day-23-clean-boundaries
昨天新增的 Boundary Test 看起來已經禁止 Use Case 引用 EF Core、WorkItemsDbContext、EF Entity 與通知 SDK。可是它選擇檔案的方式,簡化後是:
var useCasesPath = Path.Combine(
repositoryRoot,
"src",
"WorkItems.Api",
"UseCases");
var useCaseFiles = Directory.GetFiles(
useCasesPath,
"*.cs",
SearchOption.AllDirectories);
問題不在禁止清單,而在受測集合。OverdueWorkItemProcessor.cs 負責協調整個逾期流程,卻放在 src/WorkItems.Api 根目錄,不在 UseCases 資料夾內。測試把資料夾位置當成高階政策的完整清單,因此漏掉真正的流程協調器。
src/WorkItems.Api/
├─ UseCases/ ← Boundary Test 只掃這裡
│ └─ ...
└─ OverdueWorkItemProcessor.cs ← 實際流程協調器,沒有被掃到
依賴掃描至少要交代三件事:
原本測試只有 Rule,Discovery 又依賴不完整的資料夾位置。禁止清單寫得再完整,也碰不到根目錄的 Processor。
如果只展示修正後綠燈,讀者只能知道最後通過,無法確認規則是否真的具備辨錯能力。因此我固定同一個 DbContext 違規,先讓舊規則漏過,再補強 Discovery 取得紅燈,最後才移除違規。這種方式稱為受控違規(Controlled Violation)。
| 階段 | 保持不變 | 刻意改變 | 要回答的問題 |
|---|---|---|---|
| 假綠燈 | DbContext 違規與既有行為 |
不修改舊測試 | 舊測試真的看得到違規嗎? |
| 真紅燈 | 保留相同違規 | 補完整發現範圍 | 新規則能不能指出同一項違規? |
| 真綠燈 | 新規則保持不變 | 移除外層依賴 | 修正後能否同時通過結構與行為驗證? |
模型固定使用 Codex GPT-5.6-SOL-HIGH,並在單一隔離 Worktree 內執行。Dependency Rule、受控違規與三階段順序由 User 事先定義。Agent 依序重現盲點、補出紅燈,再完成修正;本次沒有測試 Agent 能否自行診斷整套架構。
你正在 AI-CleanCode-API-Demo 的隔離 Worktree 中執行正式實驗。
固定實驗條件:
- 起始 Commit 必須是 3630a0adcd0de17c70b5740948dbb2b49f26dc47。
- 模型固定為 gpt-5.6-sol,Reasoning effort 固定為 high。
- 先閱讀 AGENTS.md、Solution、Production Code 與既有測試,再修改。
- 不得修改 HTTP Route、Method、Status Code、Request/Response JSON、
SQLite Schema、Outbox、Retry、Cancellation、lost ACK、並行 Claim
或 Idempotency Key 語意。
- 不得新增 NuGet 套件、Migration、外部網路呼叫、微服務或新部署 Artifact。
- 必須依序保存假綠燈、紅燈與修正後綠燈,不能直接跳到最終答案。
- 不要 Commit 或建立 Tag,由主流程複驗後決定接受版本。
Repository 上下文與待驗證假設:
- Day 23 已將 EF Core 與通知 SDK 留在外層 Adapter。
- 現有 ProductionBoundaryTests 只掃描 src/WorkItems.Api/UseCases。
- OverdueWorkItemProcessor.cs 位於該目錄之外。
- 待驗證假設是:Processor 直接接收 WorkItemsDbContext 後,
現有 Architecture Test 仍可能通過。
第一階段:重現假綠燈
1. 在不改變外部行為的前提下,刻意讓 Processor 直接引用並接收 DbContext。
2. 不要先修改 ProductionBoundaryTests。
3. 執行既有 Production Boundary Test 與必要 Build。
4. 若測試仍通過,記錄掃描範圍造成的假綠燈;若失敗,依證據調整推論。
第二階段:讓同一個違規轉成紅燈
1. 重新盤點真正負責應用程式流程與高階政策的型別。
2. 補強 Architecture Test 或調整高階政策的 Namespace/位置。
3. 暫時保留 DbContext 直接依賴。
4. 目標測試必須留下能指出外層技術引用的紅燈。
第三階段:修正為真正的綠燈
1. 移除 Processor 對 DbContext 的直接依賴。
2. Processor 只依賴高階政策需要的 Port;跨邊界資料由 Use Case 擁有。
3. EF Core、SDK 與 Adapter 留在外層,由 Program.cs 組裝。
4. 不得把 Clean Architecture 四個圓圈照抄成固定 Project 範本。
5. Architecture Test 要同時保護協調器、Port 與跨邊界資料。
必須說明:
- 執行時控制流與原始碼依賴方向。
- 外層 Adapter 為何能在執行時被呼叫,高階政策卻不引用它。
- Architecture Test 能證明與不能證明的範圍。
- 何時使用 Source/Namespace Test,何時升級成獨立 Assembly。
- 本次沒有新增的架構與停止理由。
完成後執行 Locked Restore、Release Build、完整測試、Format、
套件弱點掃描與系列 Smoke,並回報修改、依賴方向、跨界資料、
Composition Root、完整 Gate 與 Architecture Test 的盲點。
逐字 Prompt、Session 原始輸出與重跑資訊,都放在文章最後的公開實驗紀錄中。
Agent 先加入一個只為重現問題而存在的受控違規:
// 受控違規:只用於重現 Architecture Test 的掃描盲點。
public sealed class OverdueWorkItemProcessor(
WorkItemsDbContext database,
IOverdueWorkItemMarker overdueWorkItemMarker,
INotificationOutboxDispatcher notificationOutboxDispatcher)
: IOverdueWorkItemProcessor
{
// 原有執行流程保持不變。
}
database 雖然沒有被使用,建構式一加上去,OverdueWorkItemProcessor 就必須引用 WorkItemsDbContext 與 WorkItems.Api.Data。未來 Persistence 型別改名、搬移或替換時,高階協調器也會被迫修改。
這個參數刻意不參與執行流程。Dependency Rule 檢查的是原始碼依賴,只要建構式出現 WorkItemsDbContext,編譯時耦合就已成立。若一定要等 Processor 實際呼叫 database.WorkItems 才算違規,就混淆了結構與行為兩種問題。
執行結果如下:
| 觀察項目 | 實驗結果 |
|---|---|
| Production Code | Processor 可以正常編譯 |
| 既有邊界檢查 | 沒有回報任何違規 |
| 實際原始碼依賴 | Processor 已直接認識 WorkItemsDbContext |
這就是假綠燈:Build 通過,名稱叫 ProductionBoundaryTests 的測試也通過,但受測集合根本沒有包含 Processor。只看最後一行摘要,很容易把「沒有被檢查」誤讀成「沒有違規」。

圖:違規 Processor 位於掃描範圍外;測試綠燈只代表 UseCases 資料夾內沒有被發現的違規。
我先保留 DbContext 違規,回頭定義哪些型別應該受到 Dependency Rule 保護。
Agent 把 Processor、Input Port 與處理摘要放入:
WorkItems.Api.UseCases.OverdueProcessing
接著把檢查改成兩層:
WorkItems.Api.UseCases.* Namespace 擁有。.cs 檔,再依 Namespace 選出 Use Case Source,禁止它引用外層技術。var highLevelPolicySources = Directory
.GetFiles(productionPath, "*.cs", SearchOption.AllDirectories)
.Select(path => new
{
Path = path,
Source = File.ReadAllText(path)
})
.Where(file => UseCasesNamespacePattern().IsMatch(file.Source))
.ToArray();
Assert.True(
highLevelPolicySources.Length > 0,
"Architecture Test 設定錯誤:沒有找到 Use Case Production Source。");
Length > 0 是測試本身的防呆。路徑或 Regex 寫錯時,測試必須直接失敗,不能掃描零個檔案後回報成功。它還不能證明所有高階政策都被找到,至少攔下「檢查集合完全是空的」這種假綠燈。
Use Case Source 引用檢查這次明確轉紅:
src\WorkItems.Api\UseCases\OverdueProcessing\OverdueWorkItemProcessor.cs:
禁止引用 WorkItems.Api.Data
src\WorkItems.Api\UseCases\OverdueProcessing\OverdueWorkItemProcessor.cs:
禁止引用 WorkItemsDbContext
第二階段仍保留相同 DbContext 建構式依賴。違規內容沒有更換,只有 Discovery 開始涵蓋 Processor,因此紅燈可以直接歸因於掃描範圍被補完整。
實驗中還出現另一種假綠燈。Agent 第一次使用 xUnit v3 不支援的舊式 --filter,Runner 回報未知參數,實際執行數量是零。這次結果沒有算進任何綠燈;正式驗證改用 --filter-class,並加入最低測試數檢查。成功摘要必須能回答「到底執行了什麼」。

圖:先找出所有 Production Source,再依 Namespace 篩選;非空檢查防止零檔案假綠燈,非法依賴因此真正轉紅。
規則能抓到違規後,Agent 才移除 Processor 的 DbContext 依賴。協調器只保留兩個 Use Case 真正需要的 Port:
OverdueWorkItemProcessor → IOverdueWorkItemMarker ← EfCoreOverdueWorkItemMarker
OverdueWorkItemProcessor → INotificationOutboxDispatcher ← EfCoreNotificationOutboxDispatcher
兩個 Interface 都由 Use Case 一側定義。Processor 描述自己需要的能力,EF Core Adapter 在外層實作,Program.cs 再把兩邊組裝起來。
public sealed record OverdueProcessingSummary(
int ProcessedCount,
int NotificationAttemptCount,
int NotificationFailureCount,
Guid[] FailedNotificationWorkItemIds);
public interface IOverdueWorkItemProcessor
{
Task<OverdueProcessingSummary> ProcessAsync(
CancellationToken cancellationToken);
}
public sealed class OverdueWorkItemProcessor(
IOverdueWorkItemMarker overdueWorkItemMarker,
INotificationOutboxDispatcher notificationOutboxDispatcher)
: IOverdueWorkItemProcessor
{
public async Task<OverdueProcessingSummary> ProcessAsync(
CancellationToken cancellationToken)
{
var processedCount = await overdueWorkItemMarker
.MarkDueWorkItemsOverdueAndCreateNotificationOutboxesAsync(
cancellationToken);
var dispatchSummary = await notificationOutboxDispatcher
.DispatchPendingAsync(cancellationToken);
return new OverdueProcessingSummary(
processedCount,
dispatchSummary.NotificationAttemptCount,
dispatchSummary.NotificationFailureCount,
dispatchSummary.FailedNotificationWorkItemIds);
}
}
OverdueProcessingSummary 也由 Use Case 擁有,只包含 int、Guid[] 等 BCL 型別,沒有攜帶 MVC ActionResult、HTTP Response DTO、EF Entity 或 SDK Response。
HTTP 邊界需要的 ProcessOverdueResponse 仍留在 Contracts,由 Controller 映射:
var processingSummary = await overdueWorkItemProcessor.ProcessAsync(
cancellationToken);
return Ok(new ProcessOverdueResponse(
processingSummary.ProcessedCount,
processingSummary.NotificationAttemptCount,
processingSummary.NotificationFailureCount,
processingSummary.FailedNotificationWorkItemIds));
這幾行 Mapping 把責任分開:Use Case 回傳處理摘要,Controller 再轉成 HTTP Contract。未來 JSON 欄位改動時,Processor 不必跟著修改。

圖:Processor 只依賴由 Use Case 擁有的 Port,EF Core Adapter 從外側實作契約;修正後的綠燈才有意義。
接受版本從三個層次建立四項檢查:Use Case 型別所有權、Use Case Source 引用,以及兩條 Project Reference 規則。
| 規則 | 保護範圍 | 能攔下的例子 |
|---|---|---|
| Use Case 型別所有權 | 已知協調器、Port、政策與跨界資料 | Processor 被放回 API 根目錄或外層 Namespace |
| Use Case Source 引用 | 所有 WorkItems.Api.UseCases.* Production Source |
MVC、EF Core、DbContext、EF Entity、HTTP Contract、SDK 型別滲入 |
| Service Level Project Reference | 已獨立的高階政策 Assembly | 高階政策反向新增外層 Project 或 Package Reference |
| 模擬 SDK Project Reference | 外部 SDK 模擬 Project | SDK 反向引用 API Project |
Source Rule 的禁止清單包含:
var forbiddenReferences = new[]
{
"Microsoft.AspNetCore",
"Microsoft.EntityFrameworkCore",
"WorkItems.Api.Contracts",
"WorkItems.Api.Data",
"WorkItemsDbContext",
"WorkItems.Api.Models",
"WorkItems.SimulatedExternalNotificationSdk"
};
這次沒有引進額外 Architecture Testing 套件,因為現有型別與規則不多,Source/Namespace Test 已足以重現真實盲點,也能產生可讀錯誤訊息。
不同檢查能支持的判斷與限制如下:
| 檢查方式 | 實際觀察什麼 | 結果能支持什麼判斷 | 仍然看不到什麼 |
|---|---|---|---|
| Source Scan | 原始碼是否出現禁止的 Namespace 或型別名稱 | 受掃描檔案沒有命中已列禁止字串 | C# 語意型別關係、間接依賴與漏列項目 |
| Namespace/Ownership Test | 已知高階型別是否位於受保護 Namespace | 協調器、Port 與跨界資料沒有離開約定位置 | 新增但尚未列入已知清單的高階型別 |
| Project Reference Test | .csproj 的 Project 與 Package Reference |
Assembly 沒有直接新增被禁止的編譯依賴 | 同一 Assembly 內部的錯誤責任與執行時行為 |
| 行為與整合測試 | 輸入、輸出、資料與副作用 | 外部可觀察行為仍符合契約 | 原始碼依賴方向是否正確 |
Source Scan 是字串與位置規則,不是 C# 語意分析器。它可能因註解或同名詞彙誤判,也可能漏掉未加入清單的新型別與間接依賴。即使依賴方向通過,測試仍無法判斷某個 Class 是否承擔錯誤責任,更不能取代行為測試。
這次只把穩定、能明確判定的禁令交給 Architecture Test,其餘仍回到 Repository Context 與 Review。
我目前保留單一 API Assembly。高階型別不多,也沒有多人同時修改或相同違規反覆出現的證據;現在拆 Project 會立刻增加 Reference、DI、建置與跨 Project 導覽成本。
因此 API 內先採 Source/Namespace Test;之後高階政策增加、多人維護、清單難以維護或違規反覆發生,再把 API Use Case 升級成獨立 Assembly。
| 專案情境 | 建議防線 | 判斷理由 |
|---|---|---|
| 小型、單一 Assembly、型別不多,邊界仍在演進 | Repository Policy+Source/Namespace Test | 改動成本低,錯誤訊息可直接對應目前規則 |
| 高階政策持續增加、多人維護、違規反覆發生 | 獨立 Assembly+Project Reference Test | 把依賴方向交給 Compiler 與專案結構保護 |
| 不同團隊或版本週期,需要獨立發布 | Package/明確 Component Boundary | 版本與團隊責任已形成真實邊界 |
| 需要獨立啟停、故障、資源或部署隔離 | Process/Deployment Boundary | 只有執行與營運壓力出現時才支付分散式成本 |
既有 WorkItems.ServiceLevel 已是獨立高階政策 Assembly,因此直接用 Project Reference 規則,檢查它不得新增外層 ProjectReference 或 PackageReference。同一個 Solution 裡,不同責任可以採用不同強度的防線。
只在 Prompt 寫「請遵守 Clean Architecture」仍然不夠。User 至少要回答:
這些資訊沒有答案時,型別所有權、邊界強度與外部行為都被交給 Agent 推測。輸出可能多拆四個 Project,也可能只換資料夾名稱,實際依賴完全不變。
能重複使用的最低規則,可以先用 AGENTS.md 格式整理:
## Dependency Rule Policy
### 開始修改前,先回報邊界
- 列出本次高階政策、外層技術、執行時控制流與原始碼依賴方向。
- 列出高階政策擁有的 Port、允許跨界的資料,以及禁止進入 Use Case 的型別。
- 若無法判斷某個型別屬於高階政策或外層細節,先停止修改並回報 Repository 證據。
### 依 Repository 情境選擇防線
- Level 1:小型、單一 Assembly、型別不多時,使用 Source/Namespace Test。
- Level 2:高階型別增加、多人維護或相同違規反覆發生時,升級成獨立 Assembly,並加入 Project/Package Reference Test。
- Level 3:出現獨立版本、團隊、故障、資源或部署需求時,才提出 Package、Process 或 Deployment Boundary。
- 回報本次選擇哪一級、依據哪些 Repository 事實,以及停止升級的理由。
### 限制原始碼依賴與跨界資料
- Use Case 只依賴自己擁有的 Port 與資料;不得引用 HTTP DTO、EF Core、DbContext、EF Entity 或第三方 SDK 型別。
- Interface 由需要該能力的高階政策擁有,具體 Adapter 在外層實作,由 Composition Root 完成綁定。
- 跨界資料使用簡單且明確的應用程式型別,不得把外層 Framework Model 當成核心 Contract。
- 不得把 Clean Architecture 四個圓圈直接套成固定 Project 範本。
### 驗證規則真的有執行
- Architecture Test 必須確認受測檔案或型別數量大於零。
- 刻意加入一個受控違規,確認規則能先出現紅燈,再移除違規取得綠燈。
- 失敗訊息必須列出檔案、型別與違反的規則。
- 完成後重跑行為測試與外部 Contract Gate;依賴調整不得改變既有行為。
這份 Policy 沒有把 Namespace Test 寫成唯一答案。Agent 必須依型別數量、違規頻率、團隊與部署壓力選擇 Level,並回報沒有繼續升級的理由。這裡仍是文章中的 Repository Instruction 範例,尚未寫入 API Demo;專案自己的高階型別清單與掃描範圍也不能全部塞進通用 Skill。
E — Explicit Intent and Boundaries 意圖明確 先要求 User 定義誰是高階政策、誰擁有 Interface。這次要保護的是 OverdueWorkItemProcessor,Port 由 Use Case 擁有,HTTP DTO 與 EF Entity 都不能成為跨界資料。
Program.cs 則是少數可以同時認識 Interface 與 Adapter 的 Composition Root。
邊界能被命名,Architecture Test 才知道該保護誰。第一階段假綠燈顯示,只列出禁止字串,卻沒有完整受保護集合,規則仍可能形同虛設。
這次出現兩種不可信綠燈:既有測試漏掉 Processor,以及測試 Runner 實際執行零項測試。User 不能只採信最後一行成功摘要,還要確認測試找到哪些檔案、執行哪些規則,以及刻意放入違規時是否真的轉紅。
因此正式規約加入三項要求:受測集合不得為空、受控違規必須先產生紅燈、測試工具必須回報符合預期的執行範圍。
N — Non-Surprising Behavior 符合預期 要求內部依賴重排後,API 回應、資料 Schema 與通知副作用仍維持原狀。Architecture Test 檢查高階政策有沒有認識外層技術;行為測試則確認相同輸入仍得到相同回應、重試與去重結果。兩種測試都通過,這次修正才算真正綠燈。
三個階段完成後,我重新執行所有 Gate,沒有直接採用 Agent 的全綠摘要:
| 驗證範圍 | 實驗結果 |
|---|---|
| 架構規則 | 受控違規會轉紅;移除 DbContext 依賴後恢復綠燈 |
| API Contract | Route、Status Code 與 Request/Response 保持不變 |
| Outbox 行為 | 儲存、Retry、Cancellation、lost ACK、並行 Claim 與 Idempotency 語意維持原狀 |
| Smoke | 第一次處理逾期項目,第二次執行沒有重複處理或通知 |
| 工程 Gate | Locked Restore、Release Build、Format Verify 與套件弱點檢查通過 |
目前能確認的是:受控違規會被規則攔下,移除違規後,既有 API 與 Outbox 行為保持穩定。新的高階政策是否都被列入保護,以及 Interface 是否放錯責任,仍需要設計 Review。
這次 Session 同時包含 Repository 探索、製造違規、補出紅燈、修正依賴與完整驗證,不能拿來換算 Dependency Rule 節省多少 Token。目前只確認一件事:補完整 Discovery 後,原本漏過的 DbContext 依賴會讓測試轉紅。
回到標題,Use Case 直接依賴 DbContext 時,Architecture Test 仍然綠燈,原因不是這項依賴突然合理,而是測試把 UseCases 資料夾誤當成完整高階政策集合,根本沒有掃到 Processor。
同一個違規先被漏過,補完整 Discovery 後轉紅,移除依賴才回到綠燈。這次真正修正的不只有 Processor,也包括 Architecture Test 對「誰是高階政策」的認識。
API 內目前先採 Source/Namespace Test;已獨立的 WorkItems.ServiceLevel 繼續由 Project Reference 規則保護。這份防線必須同時具備受保護型別所有權、非空 Discovery、禁止依賴規則,以及能讓受控違規轉紅的證據。
等高階政策增加、多人共同維護或越界反覆發生,再支付拆 Assembly 的成本。無論使用哪一層防線,Agent 建立 Interface 或測試後都不能只回報「全綠」;還要說清楚它找到了誰、檢查了什麼,以及哪些風險仍在掃描範圍之外。
接受版本已建立:
git fetch --tags
git switch --detach day-24-dependency-rule
現在 Dependency Rule 已經有可重跑的檢查與 Repository Policy,但測試只能攔住已定義的依賴,不能替人承擔設計與交付責任。明天就來看另一種更麻煩的情況:功能和測試都通過了,AI 新增的入口卻繞過 Outbox,送出兩次通知。